Skip to content

Wait for GOA's agent to start before a switch fails - #53

Merged
enowdev merged 6 commits into
enowdev:mainfrom
Tiyouw:fix/goa-switch-wait-for-agent
Nov 10, 2026
Merged

enowdev merged 6 commits into
enowdev:mainfrom
Tiyouw:fix/goa-switch-wait-for-agent

Conversation

@Tiyouw

@Tiyouw Tiyouw commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

What and why

Fixes so that changing GOD of Agents' agent works on Windows and workers' lines reach it there, plus one for reports that went without a word. Each commit is one change.

1. A switch fails before the agent has even started (daemon/goa_switch.go)

On Windows, changing GOA's agent or config in Settings (goa.switch) always failed with:

Claude Code didn't take (its agent ended right after starting: ── enowX restarted this session ──): GOD of Agents is back as it was

This happens with any agent: switching to Codex failed the same way.

Trigger. goaStaysUp watches the restarted session for goaSwitchSettle (5 s) and fails the switch as soon as agentGone says the agent is gone. On Windows the agent runs under pwsh -NoExit -Command "& '<agent>' …", and programGone reads "PowerShell has no child" as the agent having ended. PowerShell starts its child late. Measured on a Windows 11 machine with PowerShell 7 (pwsh.exe through its WindowsApps alias, -NoLogo -NoExit):

  • PowerShell started claude.exe about 1.4 s after it started (1401 ms and 1389 ms in two runs).
  • The daemon log shows the switch failing about 0.7 s after the restarted session started.
  • The screen's last line was only the restart marker. The agent never printed anything.

Before. On Windows, a wrapper running alone in the first second after start reads as the agent ended. Every switch is rolled back.

After. A switch fails only when:

  • the session itself has exited; or
  • the agent was seen running under its wrapper and is then gone; or
  • the agent was never seen and still isn't running when the settle is up. This keeps a fast-exiting agent as a failure, even when it ends between two looks.

The decision is a small pure function, agentTakes(settle, every, look).

2. Shell scripts get CRLF in a Windows checkout (.gitattributes)

hostconn embeds prepare.sh and linger.sh and runs them with bash on servers. Git for Windows defaults to core.autocrlf=true. A build made on Windows from such a checkout therefore embeds the scripts with CRLF, and adding a server fails:

hostconn: prepare …: Process exited with status 2: /bin/bash: -c: line 9: syntax error near unexpected token `$'do\r''

git ls-files --eol hostconn/prepare.sh showed i/lf w/crlf. The script embedded in the binary is CRLF when it is built from such a checkout, and LF in the release build. Release builds run on Ubuntu and are not affected. *.sh text eol=lf keeps every checkout LF.

3. The chat shows nothing when the agent goes on with an earlier conversation (daemon/transcript.go)

The chat finds a Claude Code session's conversation in one of two ways:

  • by the id its SessionStart hook names; or
  • by a file that began after the session started.

Claude Code runs no hooks on Windows (hooksSupported() is false). So a session whose agent went on with a conversation begun before the session showed nothing in its chat, because that file reads as another session's. Such a conversation comes from claude typed by hand in the session's shell, or from /resume. That is what GOA's chat showed after the switch: the terminal ran Claude Code on a conversation begun the night before, and the chat stayed on "Nothing yet".

Claude Code records, for each of its processes, the conversation it is on: ~/.claude/sessions/<pid>.json with pid and sessionId. Where an agent gets no hooks, ownTranscript now takes that record as the conversation's id, after a hook's word and before Meta.Conversation. The session's processes are looked at from its root down, so the agent comes before anything it started. The id found is kept as Meta.Conversation, so a restart goes on with it. Where hooks run, nothing changes. docs/chat-view.md says so.

4. Workers' reports wait for GOA instead of reaching it (core/agent/prompts.go)

On Windows, Claude Code runs no hooks. A line for GOA ([enowX GOA] Task … reports done) is therefore only typed in once GOA is idle and agent.PromptEmpty is sure its composer is empty (goaCanType). ConPTY writes a rule as wide as the screen with no line break after it. The terminal keeps the next row as that rule's soft-wrapped rest, and Text() joins them. Claude Code's composer then reads as ────…❯, which promptAlone doesn't match, so the composer always read as holding a draft. Reports reached the daemon, then waited until GOA was asked to look; once it settled the task they were dropped as moot. It was intermittent, because ConPTY doesn't always join the rows.

tailLines now cuts a rule drawn across the screen (8 or more ─━═) from what was written right after it. A line that only starts with a dash or two, such as ─ Worked for 1m 03s ─, stays whole.

5. The same, once GOA's conversation has a name (core/agent/prompts.go)

Once Claude Code names its conversation, the rule above its composer holds the name: ──── <name> ──. On Windows that rule joins the composer row as in change 4, and reads ────… <name> ─❯. The title between the rule's runs kept that from reading as a rule then a composer, so every line for GOA waited again from the moment its conversation got a name. Seen on the machine running this branch: no [enowX GOA] line reached GOA after its conversation was named, while reports arrived on the board and lines piled up in goa-tell.json with GOA idle.

splitWrappedRule now takes a title set off by spaces between a rule's runs as part of the rule. What follows a rule's run with no space, as the composer's mark does, is never a title, so a draft's own dashes stay in the draft.

6. A report waiting for GOA went without a word once its task was settled (daemon/goa.go)

docs/goa.md said that of a settled task only a stall line is dropped, and a report isn't. The code dropped every line of a settled task, so a report GOA hadn't been given yet was gone once the task was settled by hand. That is how the reports in change 5 ended: GOA settled their tasks after being asked, and the reports were dropped. A line holding a report (goaLine.Report, kept when a report and its turn's end join) now goes all the same, saying the task is settled already. Lines that only say to look (a stall, a turn's end, a question) are still dropped as moot. The docs now say which.

Used on Windows

A Windows build of this branch, with each commit added as it landed, has been the one running on a Windows 11 machine since 9 Nov:

  • Changing GOA from Codex to Claude Code in Settings now takes. Before, every switch was rolled back.
  • Adding a Linux server works with the LF scripts. Before, prepare failed on $'do\r'.
  • GOA's chat follows the conversation its terminal runs.
  • Workers' goa report lines reach GOA by themselves when it is idle, without GOA being asked to look. They stopped again once GOA's conversation got a name, which change 5 fixes.

CI

On 96547ea6, before changes 5 and 6: windows, test (macos-latest) and cloud-postgres pass. test (ubuntu-latest) fails only in cli. All six of its failures are session: load terminal emulator: downloading libghostty-vt.so: … 504 Gateway Timeout from the MyGo release download, before any of this PR's code runs. daemon, including the end-to-end switch tests in goa_switch_test.go, passes there. main's own CI is also red on test (ubuntu-latest) at e698f39c (TestLongSessionListsStayQuick). A re-run of the ubuntu job should clear the download error.

How it was tested

  • Switch, platform-neutral (daemon/goa_switch_settle_test.go):

    • a wrapper alone, then the agent: takes;
    • agent seen, then gone: fails at once;
    • agent never seen: fails only when the settle is up;
    • session exited: fails at once;
    • agent that stays up: takes.
  • Switch, Windows (TestGOASwitchWaitsForPowerShellToStartTheAgent): a process table goes through the real Windows programGone. With pwsh -NoExit alone, then with claude.exe as its child, the switch takes. With the child seen and then gone, it fails at once.

  • Transcript (core/transcript/claude_running_test.go): ClaudeRunning reads the first of a session's processes that has a record. It ignores a record naming another pid, a half-written record, and missing ones.

  • Transcript (daemon/transcript_running_test.go): a conversation begun 12 h before the session is followed, and kept as Meta.Conversation, once its agent's process says it is on it. Before that, it isn't followed. This test fails without the change.

  • Prompt (core/agent/prompts_wrap_test.go), using the end of a real GOA screen on Windows:

    • an empty composer joined to its rule reads as empty;
    • one holding a draft doesn't;
    • the turn's end is still seen.

    This test fails without the change. TestSplitWrappedRule checks what is cut and what isn't.

  • Named rule (TestPromptEmptyUnderANamedRuleWrappedOntoIt): an empty composer under a rule holding a name reads as empty, and one with a draft doesn't. The screen GOA showed on Windows while its lines waited read as holding a draft before the change, and as empty after it.

  • Settled report (daemon/goa_settled_report_test.go): of a settled task, a report still goes with the note, while a line saying to look doesn't. A report joined to its turn's end stays a report.

  • CRLF, by hand: in a binary built on Windows before .gitattributes, the embedded prepare.sh/linger.sh matched the CRLF copy. After a fresh checkout with it, they matched the LF copy.

  • Lint: go vet and staticcheck@v0.8.2 are clean for ./daemon, ./core/transcript and ./core/agent (on the head with changes 5 and 6 too).

  • Full suite on Windows: the new tests pass. 29 daemon tests fail on Windows both with and without these changes, checked against unmodified main, for example the transcript tests that set only HOME. They are outside the Windows CI subset. The end-to-end switch tests (goa_switch_test.go) use sh and skip on Windows; I had no Linux machine to run them locally.

  • By hand, on Windows: with change 1, GOA's switch to Claude Code took.

  • make test and make lint pass (vet and staticcheck pass for the changed packages; make test not run on Linux locally)

  • Works the same for a session on this device and on a server (on Linux and macOS the agent is seen at the first look, and hooks still name the conversation)

  • Docs updated where behavior changed

  • Commits signed off (git commit -s)

🤖 Generated with Claude Code

Tiyouw and others added 4 commits November 9, 2026 09:51
A switch watched GOA's restarted session for its agent ending, and took
a wrapper running alone as the agent gone. On Windows the wrapper is
PowerShell staying on (-NoExit), which takes over a second to start the
agent, so every switch there failed within a second and GOA was put back,
to Claude Code and to Codex alike.

Now a switch fails when the session ends, or when the agent was seen
under its wrapper and then is gone. An agent not seen yet is waited for
until the settle is up, then fails if it still isn't running, so one
that ends between two looks is still caught.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Ahimsa Jenar Bramsaifstyo Kusuma <tio.rmx@gmail.com>
hostconn embeds prepare.sh and linger.sh and runs them on servers with
bash. Built on Windows from a checkout with core.autocrlf=true (Git for
Windows' default), they get CRLF and adding a server fails with
"syntax error near unexpected token `$'do\r''". Release builds run on
Ubuntu and are not affected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Ahimsa Jenar Bramsaifstyo Kusuma <tio.rmx@gmail.com>
The chat finds a Claude Code session's conversation by the id its
SessionStart hook names, else by a file begun after the session started.
Claude Code runs no hooks on Windows, so a session whose agent went on
with a conversation begun before it (claude typed by hand in its shell,
or /resume) showed nothing: that file reads as another session's. That
is what GOD of Agents' chat showed after its switch to Claude Code.

Claude Code records, for each of its processes, the conversation it is
on (~/.claude/sessions/<pid>.json). Where an agent gets no hooks, the
session's processes are looked at from its root down and the first such
record names its conversation, which is kept as the session's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Ahimsa Jenar Bramsaifstyo Kusuma <tio.rmx@gmail.com>
On Windows, ConPTY writes a line as wide as the screen with no line
break after it, so the terminal keeps the next row as that line's
soft-wrapped rest and its text joins them. Claude Code's composer then
reads as a rule followed by its prompt mark, which no bare mark matches,
so PromptEmpty never saw an empty composer. With no hooks on Windows,
lines for GOD of Agents are typed in only into an empty composer, so a
worker's report waited until GOA was asked to look, and was dropped
once it settled the task.

tailLines now cuts a rule drawn across the screen from what was written
right after it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Ahimsa Jenar Bramsaifstyo Kusuma <tio.rmx@gmail.com>
@Tiyouw
Tiyouw marked this pull request as ready for review November 9, 2026 09:21
@enowdev

enowdev commented Nov 9, 2026

Copy link
Copy Markdown
Owner

Thanks — this is careful work, and each commit stands on its own. I checked it on Linux: the changed packages pass, and each new test fails with its fix reverted (the composer one at prompts_wrap_test.go:27, the chat one at transcript_running_test.go:37). Nothing should change on macOS or Linux beyond one thing: an agent that dies within a third of a second now rolls back after the 5 s settle instead of at once, which is fine. The ubuntu failure is the libghostty 504 only; I'll re-run it and merge once it's green.

Tiyouw and others added 2 commits November 10, 2026 07:24
Once Claude Code names its conversation, the rule above its composer holds
the name ("──── name ──"). On Windows that rule joins the composer row as
before, and the title between its runs kept the line from reading as an
empty composer: every line for GOD of Agents waited from then on, workers'
reports included, until the task was settled by hand and they went as moot.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Ahimsa Jenar Bramsaifstyo Kusuma <tio.rmx@gmail.com>
The docs said a report waiting for GOA isn't dropped when its task is
settled, but every line of a settled task was: a report GOA hadn't seen
went without a word once the task was settled by hand. A report now goes
all the same, saying the task is settled already; only lines saying to
look (a stall, a turn's end, a question) are dropped as moot.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Ahimsa Jenar Bramsaifstyo Kusuma <tio.rmx@gmail.com>
@enowdev
enowdev merged commit e9094a5 into enowdev:main Nov 10, 2026
2 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants